feat: 어드민 코스 플랜 구성항목 UX 개선#686
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough코스 관리 폼에서 "가격/얼리버드" 입력 필드를 제거하고 "기간(일)" 필드를 추가합니다. 동시에 플랜 항목 관리를 문자열 기반 직렬화에서 구조화된 배열 기반으로 전환하고, 숫자 입력 검증을 강화하며, 대표 플랜 중복을 방지합니다. Changes코스 플랜 관리 구조화
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/components/admin/courses/admin-course-plan-management.tsx (1)
252-255: ⚡ Quick win컴포넌트 내부 토스트 호출은 훅으로 꺼내서 재사용해주세요.
handleCreatePlan/handleUpdatePlan안에서useToastStore.getState()를 직접 호출하면 이 파일만 예외적인 패턴이 됩니다. 컴포넌트 상단에서const showToast = useToastStore((state) => state.showToast);를 가져와 재사용하는 쪽이 가이드와도 맞습니다. As per coding guidelines, "UseuseToastStoreto show toast notifications. CalluseToastStore((state) => state.showToast)inside React, oruseToastStore.getState().showToast()outside React (e.g., axios interceptors, mutation callbacks)".Also applies to: 279-282
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/admin/courses/admin-course-plan-management.tsx` around lines 252 - 255, The toast calls inside handleCreatePlan and handleUpdatePlan use useToastStore.getState() directly; instead, at the top of the component pull the hook into a reusable function reference (e.g., const showToast = useToastStore(state => state.showToast);) and replace useToastStore.getState().showToast(...) invocations (including the call guarded by hasRecommendedPlanConflict(createForm) and the similar block around lines ~279-282) with showToast(...) so the component follows the guideline of using the hook inside React.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/admin/courses/admin-course-plan-management.tsx`:
- Around line 234-245: The conflict check in hasRecommendedPlanConflict
incorrectly reads other plans' unsaved local states from editForms (via
toPlanFormValues) causing false negatives; update hasRecommendedPlanConflict to
only consider the persisted value on each plan (plansQuery.data) when
determining if another plan is already recommended — i.e., keep the early return
when targetForm.isRecommended !== 'true', then iterate plansQuery.data and
ignore the plan with plan.planId === targetPlanId but for all others compare
plan.isRecommended (the saved property) directly instead of using editForms or
toPlanFormValues.
- Around line 557-560: The rendered list in PlanItemsEditor uses unstable
key={index}, causing input state reuse when removeItem/addItem reorder elements;
add a unique editor-only id field to PlanItemFormValue (e.g., id: string) when
items are created/loaded and update any addItem/removeItem logic to preserve or
generate that id, then change the mapping to use key={item.id ?? index} (or
require id) instead of key={index} so each row has a stable React key; update
any constructors/parsers that create PlanItemFormValue to assign a UUID or
unique string to item.id.
---
Nitpick comments:
In `@src/components/admin/courses/admin-course-plan-management.tsx`:
- Around line 252-255: The toast calls inside handleCreatePlan and
handleUpdatePlan use useToastStore.getState() directly; instead, at the top of
the component pull the hook into a reusable function reference (e.g., const
showToast = useToastStore(state => state.showToast);) and replace
useToastStore.getState().showToast(...) invocations (including the call guarded
by hasRecommendedPlanConflict(createForm) and the similar block around lines
~279-282) with showToast(...) so the component follows the guideline of using
the hook inside React.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4b57cef1-21f1-4ba4-9562-df01515ae08c
📒 Files selected for processing (2)
src/components/admin/courses/admin-course-form-sections.tsxsrc/components/admin/courses/admin-course-plan-management.tsx
💤 Files with no reviewable changes (1)
- src/components/admin/courses/admin-course-form-sections.tsx
| const hasRecommendedPlanConflict = ( | ||
| targetForm: PlanFormValues, | ||
| targetPlanId?: number, | ||
| ) => { | ||
| if (targetForm.isRecommended !== 'true') return false; | ||
|
|
||
| return ( | ||
| plansQuery.data?.some((plan) => { | ||
| if (plan.planId === targetPlanId) return false; | ||
| const planForm = editForms[plan.planId] ?? toPlanFormValues(plan); | ||
| return planForm.isRecommended === 'true'; | ||
| }) ?? false |
There was a problem hiding this comment.
대표 플랜 충돌 검사가 저장되지 않은 다른 폼 상태까지 반영됩니다.
다른 플랜의 editForms 값을 그대로 읽어서, 아직 저장되지 않은 "대표 해제/설정"이 충돌 판단에 섞입니다. 예를 들어 기존 대표 플랜 A를 폼에서만 일반으로 바꿔둔 뒤 저장하지 않고 플랜 B를 대표로 저장하면 이 검사를 통과해 서버에는 대표 플랜이 2개 남을 수 있습니다. 여기서는 다른 플랜의 로컬 편집값이 아니라, 실제 저장된 plan.isRecommended만 기준으로 막아야 합니다.
🐛 제안 수정안
const hasRecommendedPlanConflict = (
targetForm: PlanFormValues,
targetPlanId?: number,
) => {
if (targetForm.isRecommended !== 'true') return false;
return (
plansQuery.data?.some((plan) => {
if (plan.planId === targetPlanId) return false;
- const planForm = editForms[plan.planId] ?? toPlanFormValues(plan);
- return planForm.isRecommended === 'true';
+ return plan.isRecommended;
}) ?? false
);
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/admin/courses/admin-course-plan-management.tsx` around lines
234 - 245, The conflict check in hasRecommendedPlanConflict incorrectly reads
other plans' unsaved local states from editForms (via toPlanFormValues) causing
false negatives; update hasRecommendedPlanConflict to only consider the
persisted value on each plan (plansQuery.data) when determining if another plan
is already recommended — i.e., keep the early return when
targetForm.isRecommended !== 'true', then iterate plansQuery.data and ignore the
plan with plan.planId === targetPlanId but for all others compare
plan.isRecommended (the saved property) directly instead of using editForms or
toPlanFormValues.
| items.map((item, index) => ( | ||
| <div | ||
| key={index} | ||
| className="border-border-subtle grid grid-cols-12 items-start gap-75 border-t px-100 py-75" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the exact map block around the reported lines
FILE="src/components/admin/courses/admin-course-plan-management.tsx"
echo "=== Context around ${FILE}:557-590 ==="
nl -ba "$FILE" | sed -n '540,590p'
# 2) Find the type/definition for PlanItemFormValue (or nearby form value type) and inspect its shape
echo
echo "=== Search for PlanItemFormValue in ${FILE} ==="
rg -n "PlanItemFormValue" "$FILE" || true
# 3) If there is a local type in this file, print the relevant region
echo
echo "=== Show nearby type definitions involving PlanItemFormValue ==="
# Grab a broader window around the first match if exists
MATCH_LINE=$(rg -n "type PlanItemFormValue|interface PlanItemFormValue|PlanItemFormValue" "$FILE" | head -n 1 | cut -d: -f1 || true)
if [[ -n "${MATCH_LINE:-}" ]]; then
START=$((MATCH_LINE-60))
END=$((MATCH_LINE+120))
nl -ba "$FILE" | sed -n "${START},${END}p"
else
echo "No PlanItemFormValue match found to expand."
fi
# 4) Locate items source/type for this items.map block (search for 'items' declaration near the map)
echo
echo "=== Search for declaration/assignment of items near the map block ==="
nl -ba "$FILE" | sed -n '500,560p'
rg -n "const items|let items|items =" "$FILE" | head -n 20 || true
# 5) Print the immediate block above items.map to see what 'items' contains
echo
echo "=== Print the code immediately above the map usage ==="
nl -ba "$FILE" | sed -n '520,565p'Repository: code-zero-to-one/study-platform-client
Length of output: 236
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/components/admin/courses/admin-course-plan-management.tsx"
echo "=== Context around ${FILE}:540-610 (with line numbers via awk) ==="
awk -v start=540 -v end=610 'NR>=start && NR<=end {printf "%d\t%s\n", NR, $0}' "$FILE"
echo
echo "=== Search for PlanItemFormValue in ${FILE} ==="
rg -n "PlanItemFormValue" "$FILE" || true
echo
echo "=== Search for type/definition of items.map block variable `items` ==="
# Look for likely declarations near the first map usage
rg -n "items\.map\(" "$FILE" || true
rg -n "const items|let items|items\s*=" "$FILE" || true
echo
echo "=== Print the block right above items.map usage (if found) ==="
MAP_LINE=$(rg -n "items\.map\(" "$FILE" | head -n 1 | cut -d: -f1 || true)
if [[ -n "${MAP_LINE:-}" ]]; then
START=$((MAP_LINE-70))
END=$((MAP_LINE+40))
echo "First items.map at line: $MAP_LINE"
awk -v start=$START -v end=$END 'NR>=start && NR<=end {printf "%d\t%s\n", NR, $0}' "$FILE"
else
echo "No items.map(...) match found."
fiRepository: code-zero-to-one/study-platform-client
Length of output: 7252
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/components/admin/courses/admin-course-plan-management.tsx"
# Show around updateItem and the form state wiring
echo "=== Context around updateItem / parsePlanItems wiring (lines 430-540) ==="
awk -v start=430 -v end=560 'NR>=start && NR<=end {printf "%d\t%s\n", NR, $0}' "$FILE"
echo
echo "=== Search for row removal/add handlers near this map ==="
rg -n "remove|delete|splice|filter|add.*item|append|push" "$FILE" || true
echo
# If there is a remove function, print region around it
echo "=== Show contexts for likely handlers (first 5 matches) ==="
rg -n "remove|delete|splice|filter|push|append" "$FILE" | head -n 5 | cut -d: -f1 | while read -r L; do
START=$((L-25)); END=$((L+25))
echo "--- Around line $L ($START-$END) ---"
awk -v start=$START -v end=$END 'NR>=start && NR<=end {printf "%d\t%s\n", NR, $0}' "$FILE"
echo
doneRepository: code-zero-to-one/study-platform-client
Length of output: 11873
동적 행 목록에서 key={index}를 안정 키로 교체 필요
PlanItemsEditor에서 items.map((item, index) => ...)로 렌더링하며 key={index}를 사용합니다. 또한 removeItem/addItem로 배열 인덱스가 바뀌기 때문에 행의 입력 상태(포커스/IME/커서 등)가 다음 행으로 재사용될 위험이 있습니다. 현재 PlanItemFormValue에는 id 같은 고유 식별자가 없으니, 편집기 전용 id를 PlanItemFormValue에 추가하고 key={item.id ?? index}로 바꾸는 편이 안전합니다.
items.map((item, index) => (
<div
key={index}
className="border-border-subtle grid grid-cols-12 items-start gap-75 border-t px-100 py-75"
>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/admin/courses/admin-course-plan-management.tsx` around lines
557 - 560, The rendered list in PlanItemsEditor uses unstable key={index},
causing input state reuse when removeItem/addItem reorder elements; add a unique
editor-only id field to PlanItemFormValue (e.g., id: string) when items are
created/loaded and update any addItem/removeItem logic to preserve or generate
that id, then change the mapping to use key={item.id ?? index} (or require id)
instead of key={index} so each row has a stable React key; update any
constructors/parsers that create PlanItemFormValue to assign a UUID or unique
string to item.id.
Summary
API
AdminCoursePlanUpsertRequest.items[]요청 shape 유지AdminCourseContentController의GET/POST/PUT/PATCH /api/v5/admin/courses/{courseId}/plans*계약과 대조 확인Validation
yarn prettier:fix(성공, 기존.codex/skills/clarify.mdsymlink warning 출력)yarn lint:fix(성공, 기존 repo warnings만 출력)yarn typecheck(성공)git diff --check(성공)Summary by CodeRabbit
변경 사항
새로운 기능
개선 사항